<?php
/**
 * Хранение и работа с данными о ресурсах (адресах) в сети
 *
 * @ingroup Models
 */
class Url extends Record {

    function setTableDefinition() {
        $this->setTableName("urls");
        $this->hascolumn("id", "integer", array("unsigned" => TRUE, "primary" => TRUE, "notnull" => TRUE, "autoincrement" => TRUE));
        $this->hasColumn("hash", "string", array("length" => LENGTH_MD5));
        $this->hasColumn("host_id", "integer", array("unsigned" => TRUE, "notnull" => TRUE));

        $this->hasColumn("url",  "string", array("length" => LENGTH_URL));

        $this->index("url_hash", array("fields" => array("hash"), "type" => "unique"));
    }

    function setUp() {
        $this->hasOne("Host as host", array(
            "local"   => "host_id",
            "foreign" => "id",
            "onDelete" => "CASCADE",
        ));
    }

    public function preInsert($event) {
        $hostname = parse_url($this->url, PHP_URL_HOST);
        $host = Doctrine::getTable("Host")->getByName($hostname);
        $this->host = $host;
    }

    public function preUpdate($event) {
        $this->preInsert(NULL);
    }

    public function create($url) {
        parent::create(array(
            "url"   => self::normalize($url),
            "hash"  => md5($url),
        ));
    }

    public function __toString() {
        return $this->getUrl();
    }

    public function getUrl() {
        return $this->url;
    }

    /**
     * @return Host
     */
    public function getHost() {
        return $this->host;
    }

    //// Namespace functions ////

    /**
     * Нормализует URL
     * @param string $url
     * @param bool $no_query обрезать GET-запрос (?xxx)
     * @param bool $no_anchor обрезать anchor (#xxx)
     * @return string
     */
    public static function normalize( $url, $no_query = FALSE, $no_anchor = FALSE ) {
        if ( empty( $url) ) {
            return NULL;
        }

        if ( strpos( $url, '://' ) === FALSE ) {
            $url = 'http://' . $url;
        }

        $parts = @parse_url( $url );

        if ( empty( $parts ) ) {
            return NULL;
        }

        $scheme = strtolower( @$parts[ "scheme" ] );
        $port = ifsetor( $parts[ 'port' ], "" );
        if ( empty( $scheme ) ) {
            $scheme = "http";
        }
        if ( $scheme == 'http' && $port == 80 ) {
            $port = '';
        } else if ( $scheme == 'https' && $port == 443 ) {
            $port = '';
        }

        if ($parts["scheme"] == "avatar") {
            return $parts["scheme"] . "://" . ifsetor($parts["host"]) . ifsetor($parts["path"]);
        }

        $parts["path"] = rtrim( strval(ifsetor($parts["path"])), "/" ); //Убираем завершающий слеш
        if( empty($parts["path"]) ) {
            $parts["path"] = '/';
        }

        $result = $scheme.'://'                  //протокол URL
               .strtolower( $parts[ "host" ] )     //имя хоста
               .( empty( $port )                  //порт подключения
                   ? ''
                   : ':'.$port )
               .ifsetor( $parts[ 'path' ], '/' )   //путь внутри домена
               .( ( empty( $parts[ 'query' ])  || $no_query )        //строка параметров
                   ? ''
                   : '?'.$parts[ 'query' ] )
               .( ( empty( $parts[ 'fragment' ] ) || $no_anchor )       //якорь ссылок
                   ? ''
                   : '#'.$parts[ 'fragment' ] );

        static $validator;
        if( !isset($validator) ) {
            $validator = new Zend_Validate_Url();
        }
        if( $validator->isValid($result)) {
            return $result;
        }
        return NULL;
    }

    /**
     * Возращает URL в упрощённом, удобочитаемом для человека виде
     * @param string $url
     * @return string
     */
    public static function normalizeHuman($url) {
        if (FALSE === strpos($url, ":/")) { // parse_url не работает без схемы
            $url = "http://" . $url;
        }
        $parts = parse_url($url);

        if (FALSE === $parts) {
            throw new Exception(_("Неправильный URL") . " " . $url);
        }

        extract($parts);

        if (isset($user) || isset($pass)) {
            throw new Exception(_("В URL не должны использоваться имя пользователя или пароль")); // возможно, когда-нибудь понадобится, тогда и сделаем
        }
        if (!isset($host)) {
            throw new Exception(_("В URL должно присутствовать имя хоста"));
        }
        if (!empty($scheme)) {
            if ("http" == $scheme) { // http:// отбросим
                unset($scheme);
            }
        }
        if (empty($scheme) && !empty($port) && (80 == $port)) { // порт 80 для HTTP отбросим
            unset($port);
        }
        if (!empty($path) && ("/" == $path) && empty($query) && empty($fragment)) { // отбросим завершающий слэш после хоста
            unset($path);
        }

        // собираем воедино
        $url = "";
        if (!empty($scheme)) {
            $url .= $scheme . "://";
        }
        $url .= $host;
        if (!empty($port)) {
            $url .= ":" . $port;
        }
        if (!empty($path)) {
            $url .= $path;
        }
        if (!empty($query)) {
            $url .= "?" . $query;
        }
        if (!empty($fragment)) {
            $url .= "#" . $fragment;
        }

        return $url;
    }

    /**
     * Возращает URL в упрощённом виде, пригодном для использовании в URL чата
     * @param string $url
     * @return string
     */
    public static function normalizeForUrl($url) {
        if (FALSE === strpos($url, ":/")) { // parse_url не работает без схемы
            $url = "http://" . $url;
        }
        $parts = parse_url($url);

        if (FALSE === $parts) {
            throw new Exception(_("Неправильный URL") . " " . $url);
        }

        extract($parts);

        if (empty($host)) {
            throw new Exception(_("В URL должно присутствовать имя хоста"));
        } else {
            $host = urldecode($host);
        }
        if (!empty($port) && (80 == $port)) { // порт 80 для HTTP отбросим
            unset($port);
        }
        if (!empty($path)) {
            if ((1 != strlen($path)) && ("/" == substr($path, -1))) { // отбросим завершающий слэш в конце пути
                $path = substr($path, 0, -1);
            }
            $path = urldecode($path);
        } else {
            $path = "/";
        }

        if (!empty($query)) {
            $query_parts = explode("&", $query);
            foreach ($query_parts as &$query_part) {
                $var_parts = explode("=", $query_part);
                foreach ($var_parts as &$var_part) {
                    $var_part = urldecode($var_part);
                    $var_part = str_replace(array(" ", "&", "=", "#"), array("+", "%26", "%3D", "%23"), $var_part);
                }
                $query_part = implode("=", $var_parts);
            }
            $query = implode("&", $query_parts);
        }

        // собираем воедино
        $url = "";
        if (!empty($scheme)) {
            $url .= $scheme . "://";
        }
        $url .= $host;
        if (!empty($port)) {
            $url .= ":" . $port;
        }
        if (!empty($path)) {
            $url .= $path;
        }
        if (!empty($query)) {
            $url .= "?" . $query;
        }

        return $url;
    }

    /**
     * Переводит относительный URL в абсолютный
     * @param string $url
     * @param string $base базовый адрес, относительно которого будет рассматриваться $url
     * @return string
     */
    public static function absolute( $url, $base ) {
        $base = self::normalize( $base );
        $base_parts = parse_url( $base );

        if ( ! isset( $base_parts[ 'path' ] ) ) {
            $base_parts[ 'path' ] = '/';
        }

        $url_parts = @parse_url( $url);
        $M = array(); //temp

        $url = preg_replace('#\w+://$#', "", $url); // Защита от урлов вида «http://» — кое-где попадаются

        return ( isset( $url_parts[ 'scheme' ] ) )
            ? $url
            : ( preg_match( '|^/|', ifsetor( $url_parts[ 'path'], "" ) )
                ? ifsetor( $base_parts[ 'scheme'], "" ) . '://' . $base_parts[ 'host' ] . $url
                : ( preg_match( '|^(/(.*/)?)[^/]*$|', $base_parts[ 'path' ], $M )
                    ? $base_parts[ 'scheme' ] . '://' . $base_parts[ 'host' ] . $M[ 1 ] . $url
                    : $base ) );
    }
}

/**
 * Коллекция ресурсов
 *
 * @ingroup Collections
 */
class UrlTable extends Doctrine_Table {

    public function getByUrl( $url ) {
        $obj = $this->findOneByUrl( $url );
        if (!$obj) {
            $url = Url::normalize( $url );
            if ($url) {
                $obj = new Url();
                $obj->create( $url );
            } else {
                return NULL;
            }
        }
        return $obj;
    }

    public function findOneByUrl($url) {
        $url = Url::normalize( $url );
        if( empty($url) ) {
            return NULL;
        }
        return Doctrine_Query::create()->from("Url")->where("hash = ?", md5($url))->limit(1)->fetchOne();
    }

    /**
     * Находит много идентификаторов url по заданным строкам
     */
    public function getIdsByUrl(array $urls) {
        $hashes = array();
        $normalized_urls = array();
        foreach($urls as $url) {
            $url = Url::normalize($url);
            if( !empty($url) ) {
                $hashes[] = md5(Url::normalize($url));
                $normalized_urls[] = $url;
            }
        }
        $list = Doctrine_Query::create()->from('Url')->whereIn('hash', $hashes)->execute();
        $ids = array();
        $existing_urls = array(); //Список найденных урлов
        foreach($list as $item) {
            $ids[] = $item->id;
            $existing_urls[] = $item->getUrl();
        }
        $needed_urls = array_diff($normalized_urls, $existing_urls);
        foreach($needed_urls as $url) {
            $created_url = $this->getByUrl($url);
            $ids[] = $created_url->id;
        }
        return $ids;
    }

}